---
title: "Custom Database Plugin"
description: Create your own database plugin for any database provider
icon: Hammer
---

## Installation

```package-install
npm install @hot-updater/plugin-core --save-dev
```

## Overview

Build a custom database plugin to integrate any database provider. This guide shows how to use `createDatabasePlugin` to build and configure your own database plugin.

## Creating a Database Plugin

Use `createDatabasePlugin` to build custom database plugins:

```typescript
import { createDatabasePlugin } from "@hot-updater/plugin-core";

export const myDatabase = createDatabasePlugin<MyConfig>({
  name: "myDatabase",              // Plugin identifier
  factory: (config) => ({
    // Return these required methods:
    getBundleById: async (bundleId) => { /* ... */ },
    getBundles: async (options) => ({ data: [], pagination: {} }),
    getChannels: async () => [],
    commitBundle: async ({ changedSets }) => { /* ... */ },
    onUnmount: async () => { /* optional cleanup */ }
  })
});
```

### Factory Function Return Type

Your factory function must return an object with these methods:

```typescript
{
  // Fetch single bundle by ID
  getBundleById: (bundleId: string) => Promise<Bundle | null>;

  // Fetch paginated bundles with optional filtering
  getBundles: (options: {
    where?: { channel?: string; platform?: string };
    limit: number;
    offset: number;
  }) => Promise<{
    data: Bundle[];
    pagination: PaginationInfo;
  }>;

  // Get all available channels
  getChannels: () => Promise<string[]>;

  // Commit all pending changes (batch operation)
  commitBundle: (params: {
    changedSets: {
      operation: "insert" | "update" | "delete";
      data: Bundle;
    }[];
  }) => Promise<void>;

  // Optional: cleanup resources on unmount
  onUnmount?: () => Promise<void>;
}
```

**Note**: The factory does NOT need to implement `updateBundle`, `appendBundle`, or `deleteBundle` - these are auto-generated by `createDatabasePlugin` and tracked internally.

## Implementation Example

Here's a complete custom database plugin using a REST API:

```typescript title="customDatabase.ts"
import {
  type Bundle,
  type PaginationInfo,
  createDatabasePlugin,
} from "@hot-updater/plugin-core";

export interface CustomDatabaseConfig {
  baseUrl: string;
  apiKey: string;
}

export const customDatabase = createDatabasePlugin<CustomDatabaseConfig>({
  name: "customDatabase",
  factory: (config) => {
    const headers = {
      "Content-Type": "application/json",
      Authorization: `Bearer ${config.apiKey}`,
    };

    return {
      async getBundleById(bundleId) {
        const response = await fetch(
          `${config.baseUrl}/bundles/${bundleId}`,
          { headers }
        );

        if (!response.ok) {
          if (response.status === 404) return null;
          throw new Error(`Failed to fetch bundle: ${response.statusText}`);
        }

        return response.json();
      },

      async getBundles(options) {
        const params = new URLSearchParams({
          limit: String(options.limit),
          offset: String(options.offset),
        });

        if (options.where?.channel) {
          params.set("channel", options.where.channel);
        }
        if (options.where?.platform) {
          params.set("platform", options.where.platform);
        }

        const response = await fetch(
          `${config.baseUrl}/bundles?${params}`,
          { headers }
        );

        if (!response.ok) {
          throw new Error(`Failed to fetch bundles: ${response.statusText}`);
        }

        const result = await response.json();
        return {
          data: result.data,
          pagination: result.pagination,
        };
      },

      async getChannels() {
        const response = await fetch(`${config.baseUrl}/channels`, {
          headers,
        });

        if (!response.ok) {
          throw new Error(`Failed to fetch channels: ${response.statusText}`);
        }

        return response.json();
      },

      async commitBundle({ changedSets }) {
        // Process all changes in batch
        for (const change of changedSets) {
          if (change.operation === "insert" || change.operation === "update") {
            await fetch(`${config.baseUrl}/bundles`, {
              method: "POST",
              headers,
              body: JSON.stringify(change.data),
            });
          } else if (change.operation === "delete") {
            await fetch(`${config.baseUrl}/bundles/${change.data.id}`, {
              method: "DELETE",
              headers,
            });
          }
        }
      },

      async onUnmount() {
        // Optional: cleanup resources, close connections, etc.
        console.log("Database plugin unmounted");
      },
    };
  },
});
```

## Helper Utilities

The `@hot-updater/plugin-core` package provides helper functions:

### calculatePagination

Generates pagination metadata from total count and options:

```typescript
import { calculatePagination } from "@hot-updater/plugin-core";

const pagination = calculatePagination(totalCount, {
  limit: 10,
  offset: 0,
});
// Result: { total, limit, offset, hasMore }
```

## CLI Configuration

Use your custom plugin in `hot-updater.config.ts`:

```typescript
import { defineConfig } from "@hot-updater/core";
import { customDatabase } from "./customDatabase";

export default defineConfig({
  database: customDatabase({
    baseUrl: process.env.DATABASE_BASE_URL!,
    apiKey: process.env.DATABASE_API_KEY!,
  }),
  // ... other config
});
```

## Custom Server Usage

Use your plugin with `createHotUpdater` for self-hosted servers:

```typescript
import { createHotUpdater } from "@hot-updater/core";
import { customDatabase } from "./customDatabase";

const hotUpdater = createHotUpdater({
  database: customDatabase({
    baseUrl: process.env.DATABASE_BASE_URL!,
    apiKey: process.env.DATABASE_API_KEY!,
  }),
  // ... other options
});
```

## Best Practices

### Security

- Never hardcode credentials in plugin code
- Use environment variables for sensitive data
- Implement proper authentication headers
- Validate input data before storing

### Performance

- Implement efficient filtering in `getBundles`
- Use database indexes for common queries
- Batch operations in `commitBundle` when possible
- Cache channel lists if they don't change frequently

### Error Handling

- Return `null` from `getBundleById` when not found (don't throw)
- Throw descriptive errors for other failures
- Handle network errors gracefully
- Validate bundle data structure
